在Instagram、Facebook,或是各種電商APP中,我們都可以在列表中向下滑動來查看更多內容。這種不間斷的滑動體驗就是所謂的「無限滾動」。在手機APP的開發中,因為手機性能有限,所以如何實現這種無限滾動但又確保性能,是一門學問。
我們先從最基本的滾動組件開始,ScrollView這是一個最基礎的滾動組件,它支援橫向及縱向滾動。使用它相當簡單:只需要將子組件置於ScrollView組件的內層。當子組件的高度或寬度超過ScrollView的範圍時,使用者即可透過滑動來查看更多。
ScrollView的缺點是。它會依據列表數量直接渲染所有的列表。如果列表少還好,但是,當列表一多時,就會導致性能問題。
為了解決滾動列表性能問題,React Native 2017年推出了高性能列表組件FlatList。
FlatList底層是VirtualizedList,而VirtualizedList底層是ScrollView,
所以VirtualizedList 和 ScrollView 组件中的大部分属性,FlatList 组件也都可以使用。
FlatList和ScrollView最大的區別在於,FlatList是「按需渲染」。例如,當使用者正在看列表中的0至9項,FlatList只會渲染前0至19項。當使用者滑動到第10至19項時,FlatList會預先加載第20至29項,確保使用者瀏覽的流暢性。而當使用者繼續滾動到20至29項時,FlatList會回收、釋放最初的0至9項。
透過這種方式,FlatList可以確保在任何時刻都只渲染一小部分的列表,以節省資源,並優化性能,提供更流暢的使用者體驗。
初始化狀態:
使用useState初始化所需的狀態,列表資料、頁碼、加載狀態、所有資料加載完畢。
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [allDataLoaded, setAllDataLoaded] = useState(false);
加載Function:
使用fetchData函數來從API加載資料。這裡,我們用一個mock API jsonplaceholder來模擬。
const PAGE_SIZE = 10; // 一次10筆
const fetchData = async (pageNum) => {
// 設置加載狀態為true,以顯示加載中的UI提示
setLoading(true);
// 根據當前的頁碼計算請求API要帶的資料範圍
const start = (pageNum - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
// 發起API請求,這裡使用的是jsonplaceholder mock API
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_start=${start}&_end=${end}`);
const result = await response.json();
// 關閉加載狀態
setLoading(false);
if (result && result.length > 0) {
// 如果有資料,則將新資料加到當前資料的後面
setData(prevData => [...prevData, ...result]);
// 增加頁碼,以供下次加載時使用
setPage(pageNum + 1);
} else {
// 如果返回的資料為空,則設置allDataLoaded為true,表示所有資料已經加載完畢
setAllDataLoaded(true);
}
};
初始資料加載
在useEffect加載初次資料:
useEffect(() => {
fetchData(1);
}, []);
加載更多:
寫一個handleLoadMore Function,當用戶滾動到列表底部時觸發
const handleLoadMore = useCallback(() => {
// 如果現在沒有在加載資料且還有更多資料可以加載
if (!loading && !allDataLoaded) {
// 加載下一頁
fetchData(page);
}
// 使用 `useCallback` 確保這個function在組件更新時保持不變,,
// 除非 `loading`, `allDataLoaded`, 或 `page` 這些值有所改變。
}, [loading, allDataLoaded, page]);
底部渲染處理:
寫一個renderFooter Function來處理我們要在底部顯示的:加載中動畫、無更多資料的訊息。
const renderFooter = () => {
if (allDataLoaded) {
return <Text style={styles.infoText}>沒有更多資料了</Text>;
}
return loading ? <ActivityIndicator size="large" color="#0000ff" /> : null;
};
FlatList:
<FlatList
data={data} // 要顯示的資料
renderItem={({ item }) => <Text style={styles.itemText}>{item.title}</Text>}
keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore} // 當用戶滾動到列表的底部時觸發的Function
onEndReachedThreshold={0.2} // 在用戶滾動到距離底部20%時就觸發 `onEndReached`
ListFooterComponent={renderFooter} // 在列表的底部渲染的組件
ListEmptyComponent={<Text style={styles.infoText}>無資料</Text>} // 當 `data` 為空時要渲染的組件
/>
完成!
完整代碼
import React, { useState, useEffect, useCallback } from 'react';
import { FlatList, View, Text, ActivityIndicator, StyleSheet } from 'react-native';
const PAGE_SIZE = 10;
const Home = () => {
const [data, setData] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const [allDataLoaded, setAllDataLoaded] = useState(false);
const fetchData = async (pageNum) => {
setLoading(true);
const start = (pageNum - 1) * PAGE_SIZE;
const end = start + PAGE_SIZE;
const response = await fetch(`https://jsonplaceholder.typicode.com/posts?_start=${start}&_end=${end}`);
const result = await response.json();
setLoading(false);
if (result && result.length > 0) {
setData(prevData => [...prevData, ...result]);
setPage(pageNum + 1);
} else {
setAllDataLoaded(true);
}
};
useEffect(() => {
fetchData(1);
}, []);
const handleLoadMore = useCallback(() => {
if (!loading && !allDataLoaded) {
fetchData(page);
}
}, [loading, allDataLoaded, page]);
const renderFooter = () => {
if (allDataLoaded) {
return <Text style={styles.infoText}>沒有更多資料了</Text>;
}
return loading ? <ActivityIndicator size="large" color="#0000ff" /> : null;
};
return (
<FlatList
data={data}
renderItem={({ item }) => <Text style={styles.itemText}>{item.title}</Text>}
keyExtractor={(item) => item.id.toString()}
onEndReached={handleLoadMore}
onEndReachedThreshold={0.1}
ListFooterComponent={renderFooter}
ListEmptyComponent={<Text style={styles.infoText}>無資料</Text>}
/>
);
};
const styles = StyleSheet.create({
itemText: {
padding: 10,
borderBottomColor: '#ccc',
borderBottomWidth: 1,
},
infoText: {
padding: 10,
textAlign: 'center',
color: '#888'
}
});
export default Home;
RecyclerListView是社群開發的列表套件,與 FlatList 一樣採「按需渲染」的方式,兩者主要差別在於處理不使用元件上的策略。
在 FlatList 中,當列表項超出可視區時,它會被回收釋放,而新的列表項則會被創建。這種頻繁的新增、刪除,記憶體需要頻繁的分配和釋放,會有較高的計算和資源開銷,尤其在快速滾動時,可能會有卡頓、掉幀的現象。
而 RecyclerListView 則採用一種「重用策略」。當列表項不在可視區時,不將它刪除,而是將它重定向到新的位置。因此,與 FlatList 的新增、刪除相比,RecyclerListView 只需要調整和重用已存在的元件,大幅降低了計算量和資源開銷,所以性能上會比FlatList更勝一籌。
對RecyclerListView有興趣也可以看看作者Blog的介紹:RecyclerListView: High performance ListView for React Native and Web
在移動設備上,如何在確保流暢的滑動體驗的同時避免性能瓶頸,是許多APP開發者都會遇到問題。
從基本的ScrollView到FlatList,再到社群開發的RecyclerListView,我們可以看到不同的渲染策略和性能優化的方法。FlatList按需渲染、回收創建,以及RecyclerListView的元件重用策略,都是解決移動應用中列表滾動性能問題的一種方法。
選擇哪一種方式實現滾動列表,取決於具體的需求。如果是簡單的短列表,ScrollView就夠用了。如果是大量資料和需要無限滾動功能的場景,FlatList和RecyclerListView會是更好的選擇。總之,瞭解這些工具和它們的原理,可以幫助我們做出更合適的技術選擇,打造出高性能的APP。
https://juejin.cn/post/7252684645979242533?searchId=2023100415544745D0A35A9BDD9116FB2F